package be.mjosoft.ios.apn;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

/**
 * This class is used to send message to the Apple Push Notification Service.
 * @author mj
 */
public class APNService
{
	protected static String host = "gateway.push.apple.com";
	protected static int port = 2195;
	protected static String hostDevel = "gateway.sandbox.push.apple.com";
	protected static int portDevel = 2195;

	protected boolean develMode;
	protected SSLSocketFactory sslSocketFactory;
	protected Socket socket;

	/**
	 * Constructor.
	 * Switches into production mode.
	 */
	public APNService()
	{
		this(false);
	}

	/**
	 * Constructor.
	 * @param develMode true switches into development mode, false switches into production mode.
	 */
	public APNService(boolean develMode)
	{
		this.develMode = develMode;
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateFile the certificate file.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(String certificateFile, String certificatePWD, String certificateType) throws APNException
	{
		try{
			initConnexion(new FileInputStream(certificateFile), certificatePWD, certificateType);
		}catch(FileNotFoundException ex){
			throw new APNException(ex);
		}
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateFile the certificate file.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(File certificateFile, String certificatePWD, String certificateType) throws APNException
	{
		try{
			initConnexion(new FileInputStream(certificateFile), certificatePWD, certificateType);
		}catch(FileNotFoundException ex){
			throw new APNException(ex);
		}
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateStream the certificate file stream.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(InputStream certificateStream, String certificatePWD, String certificateType) throws APNException
	{
		try
		{
			KeyStore keyStore = KeyStore.getInstance(certificateType);
			keyStore.load(certificateStream, certificatePWD.toCharArray());
			KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("sunx509");
			keyManagerFactory.init(keyStore, certificatePWD.toCharArray());
			TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("sunx509");
			trustManagerFactory.init(keyStore);
			SSLContext sslCtx = SSLContext.getInstance("TLS");
			sslCtx.init(keyManagerFactory.getKeyManagers(), null, null);
			sslSocketFactory = sslCtx.getSocketFactory();
			connect();
		}
		catch(Exception ex)
		{
			throw new APNException(ex);
		}
	}

	/**
	 * Connects to the Apple Push Notification Service.
	 * @throws APNException if an error occured.
	 */
	protected void connect() throws APNException
	{
		try
		{
			if(develMode){
				socket = (SSLSocket)sslSocketFactory.createSocket(hostDevel, portDevel);
			}else{
				socket = (SSLSocket)sslSocketFactory.createSocket(host, port);
			}
		}
		catch(IOException ex)
		{
			throw new APNException(ex);
		}
	}

	/**
	 * Ends the connection to the Apple Push Notification Service.
	 */
	public void endConnection()
	{
		try{
			socket.close();
		}catch(Exception ex){}
	}

	/**
	 * Sends a message to a device.
	 * @param deviceToken the token of the device to send the message.
	 * @param message the message to send.
	 * @throws APNException if an error occured.
	 */
  public void sendAPN(String deviceToken, APNMessage message) throws APNException
  {
		deviceToken = deviceToken == null ? "" : deviceToken.replaceAll(" ", "").replaceAll("<", "").replaceAll(">", "");
		byte binary[] = createBinary(deviceToken, message);

		int nbTry = 3;
		while(nbTry > 0)
		{
			try
			{
				OutputStream out = socket.getOutputStream();
				out.write(binary, 0, binary.length);
				out.flush();

				break;
			}
			catch(Exception ex)
			{
				nbTry--;
				endConnection();
				try{
					connect();
				}catch(Exception exx){}
			}
		}

		if(nbTry == 0){
			// Failed.
			throw new APNException("The message cannot be send!");
		}
  }

	/**
	 * Create the binary message to send to the Apple Push Notification Service.
	 * @param deviceToken the token of the device to which send the message.
	 * @param message the message to send.
	 * @return the binary representation of the message to send.
	 */
	protected byte[] createBinary(String deviceToken, APNMessage message)
	{
		// Prepare the token part.
    deviceToken = deviceToken.toUpperCase();
    byte deviceTokenBinary[] = new byte[deviceToken.length() / 2];
    for(int i = 0, k = 0; i < deviceToken.length(); i += 2)
    {
      String byteS = deviceToken.substring(i, i + 2);
      int byteInt = Integer.parseInt(byteS, 16);
      deviceTokenBinary[k++] = (byte)byteInt;
    }

		byte[] messageBytes = null;
		try{
			messageBytes = message.toString().getBytes("UTF-8");
		}catch(Exception ex){
			ex.printStackTrace();
		}

		// Writes the bytes.
    int nbBytes = 3 + deviceTokenBinary.length + 2 + messageBytes.length;
		ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(nbBytes);

		// Command (one byte)
    bytesOut.write((byte)0);

		// Token length (2 bytes) (big endian)
    bytesOut.write((byte)(deviceTokenBinary.length & 0xff00) >> 8);
    bytesOut.write((byte)(deviceTokenBinary.length & 0xff));

		// Token (32 bytes)
		try{
			bytesOut.write(deviceTokenBinary);
		}catch(Exception ex){
			ex.printStackTrace();
		}

		// Payload length (2 bytes) (bid endian)
    bytesOut.write((byte)(messageBytes.length & 0xff00) >> 8);
    bytesOut.write((byte)(messageBytes.length & 0xff));

		// Payload (message)
		try{
	    bytesOut.write(messageBytes);
		}catch(Exception ex){
			ex.printStackTrace();
		}
    
		return bytesOut.toByteArray();
	}
}